﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

using Nova.CodeDOM;
using Attribute = Nova.CodeDOM.Attribute;

namespace Nova.UI
{
    /// <summary>
    /// Used to format code objects as WPF objects to be displayed in a UI.
    /// </summary>
    /// <remarks>
    /// This class keeps track of the borders and stackpanels as the code is rendered, including
    /// handling the indentation level as new "lines" of code are emitted.  It also supports
    /// "pending comments" that are written in block style if not the last thing on the line.  And,
    /// it keeps track of the current line number, and fires events when newlines are created.
    /// </remarks>
    public class CodeRenderer
    {
        #region /* STATICS */

        /// <summary>
        /// The default proportional font size.
        /// </summary>
        public static readonly double DefaultProportionalFontSize = SystemFonts.MessageFontSize;

        // Ensure that static init of UI objects in CodeObject is done on the main thread by referencing CodeObject here!
        protected static readonly Brush DUMMY_BRUSH = CodeObjectVM.DarkLavender;

        protected static FontFamily _codeFontFamily = new FontFamily("Courier New");
        protected static double _codeFontSize = DefaultCodeFontSize;
        protected static Typeface _codeFontTypeFace;
        protected static double _zoomLevel;     // The zoom multiple (1..N)
        protected static double _spaceWidth;    // Width of a space in pixels for the current CodeFontFamily
        protected static double _fontHeight;    // Height of the current CodeFontFamily
        protected static double _fontBaseline;  // Baseline of the current CodeFontFamily
        protected static bool _usingCourierNew;
        protected static Dictionary<string, double> _codeTextWidths = new Dictionary<string, double>(); 
        protected static double _tinyFontSize = DefaultTinyFontSize;
        protected static FontFamily _proportionalFontFamily = SystemFonts.MessageFontFamily;
        protected static double _proportionalFontSize = DefaultProportionalFontSize;
        protected static Typeface _proportionalFontTypeFace;

        /// <summary>
        /// Determines if borders should be displayed.
        /// </summary>
        public static bool ShowBorders = true;

        /// <summary>
        /// Determines if background colors should be displayed.
        /// </summary>
        public static bool ShowBackgroundColors = true;

        /// <summary>
        /// Determines if gradient shading should be used.
        /// </summary>
        public static bool UseShading = true;

        /// <summary>
        /// Determines if half-height blank lines should be used.
        /// </summary>
        public static bool HalfHeightBlankLines;

        /// <summary>
        /// Determines if borders should be used on all sub-expressions.
        /// </summary>
        public static bool MaximizeBorders;

        /// <summary>
        /// Determines if rendering should be virtualized using Canvases so that WPF objects exist only for visible items.
        /// </summary>
        public static bool VirtualizeRendering = true;

        protected static void RecalculateCodeFont()
        {
            // Clear the dictionary of code text widths and re-calculate some values
            _codeTextWidths.Clear();
            _zoomLevel = _codeFontSize / DefaultCodeFontSize;
            _codeFontTypeFace = new Typeface(_codeFontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
            FormattedText space = new FormattedText(" ", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, _codeFontTypeFace, _codeFontSize, Brushes.Black);
            _spaceWidth = space.WidthIncludingTrailingWhitespace;
            _fontHeight = space.Height;
            _fontBaseline = space.Baseline;

            // Set a flag if we're using the Courier New font
            _usingCourierNew = (_codeFontFamily.Source == "Courier New");
        }

        protected static void RecalculateProportionalFont()
        {
            _proportionalFontTypeFace = new Typeface(_proportionalFontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
        }

        /// <summary>
        /// Purge any cached data (such as widths of strings in the current font).
        /// </summary>
        public static void PurgeCachedData()
        {
            _codeTextWidths.Clear();
        }

        #endregion

        #region /* CONSTANTS */

        /// <summary>
        /// The minimum valid font size.
        /// </summary>
        public const double MinimumValidFontSize = 0.004;

        /// <summary>
        /// Default to 13px (10pt) for the code font.
        /// </summary>
        public const double DefaultCodeFontSize = 13;

        /// <summary>
        /// Default to 8px for the tiny braces font.
        /// </summary>
        public const double DefaultTinyFontSize = 8;

        #endregion

        #region /* FIELDS */

        public bool Measuring;
        public double StartY, EndY;
        public ContextMenuEventHandler ToolTipContextMenuOpening;
        public CommandBindingCollection CommandBindings;

        protected StackPanel _topMost;
        protected Border _currentBorder;
        protected StackPanel _currentBlock;
        protected Canvas _currentCanvas;
        protected WrapPanel _currentLine;
        protected TextBlock _currentLineWrapper;  // Used to wrap TextBlocks of different fonts to align their baselines
        protected bool _isNewLine = true;
        protected int _lineNumber;
        protected bool _isGenerated;
        protected Stack<BorderIndentState> _borderIndentStateStack = new Stack<BorderIndentState>();
        protected Stack<MeasureState> _measureStateStack = new Stack<MeasureState>();

        #endregion

        #region /* CONSTRUCTORS */

        static CodeRenderer()
        {
            RecalculateCodeFont();
            RecalculateProportionalFont();
            InitializeCommands();
        }

        /// <summary>
        /// Create a <see cref="CodeRenderer"/> that renders into the provided <see cref="StackPanel"/>, rendering the area
        /// defined by startY/endY.
        /// </summary>
        public CodeRenderer(StackPanel stackPanel, double startY, double endY, bool isGenerated)
        {
#if DEBUG_VIRTUALIZATION
            if (endY < double.MaxValue)
            {
                double viewHeight = endY - startY;
                double margin = viewHeight * 0.25;
                startY += margin;
                endY -= margin;
            }
#endif
            StartY = startY;
            EndY = endY;
            _topMost = _currentBlock = stackPanel;
            _isGenerated = isGenerated;
            _borderIndentStateStack.Push(new BorderIndentState(null));
            _measureStateStack.Push(new MeasureState(null, 0, 0, 0));
            NewLine();

            stackPanel.MouseMove += stackPanel_MouseMove;
            InitializeToolTipTimer();
        }

        /// <summary>
        /// Create a <see cref="CodeRenderer"/> that renders into the provided <see cref="StackPanel"/>, rendering the area
        /// defined by startY/endY.
        /// </summary>
        public CodeRenderer(StackPanel stackPanel)
            : this(stackPanel, 0, double.MaxValue, false)
        { }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The current line number.
        /// </summary>
        public int LineNumber
        {
            get { return _lineNumber; }
        }

        /// <summary>
        /// True if a newline is required before any other text, such as if a compiler directive was just emitted
        /// (used to force a newline after an "infix" compiler directive).
        /// </summary>
        public bool NeedsNewLine { get; set; }

        /// <summary>
        /// True if the code being rendered is generated (such as a generated '.g.cs' file).  Code cleanup settings will be ignored.
        /// </summary>
        public bool IsGenerated
        {
            get { return _isGenerated; }
        }

        /// <summary>
        /// The font family used to render code.
        /// </summary>
        public static FontFamily CodeFontFamily
        {
            get { return _codeFontFamily; }
            set
            {
                _codeFontFamily = value;
                RecalculateCodeFont();
            }
        }

        /// <summary>
        /// The font size for code.
        /// </summary>
        public static double CodeFontSize
        {
            get { return _codeFontSize; }
            set
            {
                _codeFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value);
                RecalculateCodeFont();

                // Automatically scale the proportional font size based upon the relative
                // change in the code font size from the default.
                ProportionalFontSize = DefaultProportionalFontSize - (DefaultCodeFontSize - _codeFontSize);
            }
        }

        /// <summary>
        /// The height of the code font.
        /// </summary>
        public static double CodeFontHeight
        {
            get { return _fontHeight; }
        }

        /// <summary>
        /// The font size used for tiny braces mode.
        /// </summary>
        public static double TinyFontSize
        {
            get { return _tinyFontSize; }
            set { _tinyFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value); }
        }

        /// <summary>
        /// The font family used to render proportional text.
        /// </summary>
        public static FontFamily ProportionalFontFamily
        {
            get { return _proportionalFontFamily; }
            set
            {
                _proportionalFontFamily = value;
                RecalculateProportionalFont();
            }
        }

        /// <summary>
        /// The font size for proportional text.
        /// </summary>
        public static double ProportionalFontSize
        {
            get { return _proportionalFontSize; }
            set
            {
                _proportionalFontSize = (value < MinimumValidFontSize ? MinimumValidFontSize : value);
                RecalculateProportionalFont();
            }
        }

        /// <summary>
        /// The width of a space for the current CodeFontFamily.
        /// </summary>
        public static double SpaceWidth
        {
            get { return _spaceWidth; }
        }

        /// <summary>
        /// The height of the current CodeFontFamily.
        /// </summary>
        public static double FontHeight
        {
            get { return _fontHeight; }
        }

        /// <summary>
        /// Returns true if CodeFontFamily is Courier New, otherwise false.
        /// </summary>
        public static bool UsingCourierNew
        {
            get { return _usingCourierNew; }
        }

        #endregion

        #region /* METHODS */

        /// <summary>
        /// A callback action used to re-render everything.
        /// </summary>
        public static Action ReRenderAll;

        /// <summary>
        /// Render a new line.
        /// </summary>
        public void NewLine(bool noIndent)
        {
            ++_lineNumber;
            NeedsNewLine = false;

            if (Measuring)
            {
                // Update the Height & Width of the current VM if necessary
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    currentVM.Height += currentMeasureState.LineHeight;
                    if (currentVM.Width < currentMeasureState.LineWidth)
                        currentVM.Width = currentMeasureState.LineWidth;
                    currentMeasureState.LineHeight = currentMeasureState.LineWidth
                        = currentMeasureState.MaxBaseline = currentMeasureState.MaxUnderBaseline = 0;
                }
            }
            else
            {
                _currentLine = null;
                _currentLineWrapper = null;
                _isNewLine = true;
            }

            // Indent if appropriate, making the pending relative indent amount into an absolute value
            if (!noIndent && _currentCanvas == null)  // Ignore indents if using a canvas
            {
                BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
                if (borderIndentState.IndentOnNewLine > 0)
                {
                    if (borderIndentState.IndentStateStack == null)
                        borderIndentState.IndentStateStack = new Stack<IndentState>();
                    if (borderIndentState.IndentStateStack.Count > 0)
                        borderIndentState.PendingIndentState.IndentPosition += borderIndentState.IndentStateStack.Peek().IndentPosition;
                    borderIndentState.IndentStateStack.Push(borderIndentState.PendingIndentState);
                    borderIndentState.IndentOnNewLine = 0;
                }
                // Render any indentation
                if (borderIndentState.IndentStateStack != null && borderIndentState.IndentStateStack.Count > 0)
                    RenderText(new string(' ', borderIndentState.IndentStateStack.Peek().IndentPosition), CodeObjectVM.PUNC_BRUSH, borderIndentState.BaseCodeObjectVM);
            }
        }

        /// <summary>
        /// Render a new line.
        /// </summary>
        public void NewLine()
        {
            NewLine(false);
        }

        /// <summary>
        /// Render the specified number of new lines.
        /// </summary>
        public void NewLines(int count, CodeObjectVM tag)
        {
            if (count > 1 && _currentCanvas == null)  // Ignore blank lines if using a canvas
            {
                NewLine(true);  // No indent spaces until the final newline below
                RenderText(new string('\n', count - 2), CodeObjectVM.PUNC_BRUSH, _codeFontFamily,
                    HalfHeightBlankLines ? (_codeFontSize / 2) : _codeFontSize, 0, TextStyle.Normal, tag);
            }
            NewLine();
        }

        /// <summary>
        /// Render a list of <see cref="CodeObject"/>s.
        /// </summary>
        public void RenderList<T>(IEnumerable<T> enumerable, CodeObjectVM.RenderFlags flags, CodeObjectVM parent) where T : CodeObjectVM
        {
            if (enumerable == null)
                return;

            // Increase the indent level for any newlines that occur within the child list unless specifically told not to
            bool increaseIndent = !flags.HasFlag(CodeObjectVM.RenderFlags.NoIncreaseIndent);
            if (increaseIndent)
                IndentOnNewLineBegin(parent);

            // Render the items in the list
            bool isSingleLine = true;
            IEnumerator<T> enumerator = enumerable.GetEnumerator();
            bool isFirst = true;
            bool hasMore = enumerator.MoveNext();
            while (hasMore)
            {
                T codeObjectVM = enumerator.Current;
                hasMore = enumerator.MoveNext();
                if (codeObjectVM != null)
                {
                    if (codeObjectVM.IsFirstOnLine)
                        isSingleLine = false;

                    // Determine if the child object has a border
                    bool childHasBorder = ((flags.HasFlag(CodeObjectVM.RenderFlags.ForceBorder) || codeObjectVM.HasBorder()) && !flags.HasFlag(CodeObjectVM.RenderFlags.NoBorder));

                    // Render the code object, omitting any EOL comments (so they can be rendered later after
                    // any comma), and prefixing a space if it's not the first item.
                    codeObjectVM.Render(this, flags | CodeObjectVM.RenderFlags.NoEOLComments | (childHasBorder ? 0 : CodeObjectVM.RenderFlags.NoPostAnnotations)
                        | (isSingleLine ? (isFirst ? 0 : CodeObjectVM.RenderFlags.PrefixSpace) : CodeObjectVM.RenderFlags.PrefixSpace));
                    flags &= ~(CodeObjectVM.RenderFlags.SuppressNewLine | CodeObjectVM.RenderFlags.NoPreAnnotations);

                    if (hasMore)
                    {
                        // Render the trailing comma, with EOL comments before or after it, depending on whether or not it's the last thing on the line
                        CodeObjectVM nextObject = enumerator.Current;
                        bool isLastOnLine = (nextObject != null && nextObject.IsFirstOnLine);
                        if (!isLastOnLine)
                            codeObjectVM.RenderEOLComments(this, flags);
                        if (!flags.HasFlag(CodeObjectVM.RenderFlags.NoItemSeparators))
                            RenderText(CodeDOM.Expression.ParseTokenSeparator, CodeObjectVM.PUNC_BRUSH, parent);
                        if (isLastOnLine)
                            codeObjectVM.RenderEOLComments(this, flags);
                    }
                    else
                    {
                        if (flags.HasFlag(CodeObjectVM.RenderFlags.HasTerminator) && !flags.HasFlag(CodeObjectVM.RenderFlags.Description) && parent != null)
                            ((StatementVM)parent).RenderTerminator(this, flags);
                        codeObjectVM.RenderEOLComments(this, flags);
                    }
                    if (!childHasBorder)
                        codeObjectVM.RenderAnnotations(this, AnnotationFlags.IsPostfix, flags);
                }
                else if (hasMore)
                    RenderText(CodeDOM.Expression.ParseTokenSeparator, CodeObjectVM.PUNC_BRUSH, parent);
                isFirst = false;
            }

            // Reset the indent level
            if (increaseIndent)
                IndentOnNewLineEnd(parent);
        }

        public void RenderVisibleList<T>(IEnumerable<T> enumerable, CodeObjectVM.RenderFlags flags) where T : CodeObjectVM
        {
            if (enumerable != null)
            {
                foreach (T codeObjectVM in enumerable)
                {
                    if (codeObjectVM != null)
                        codeObjectVM.RenderVisible(this, flags);
                }
            }
        }

        /// <summary>
        /// The number of <see cref="TextBlock"/>s created.
        /// </summary>
        public int TextBlockCount;

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, double fontSize, int verticalOffset, TextStyle style, CodeObjectVM tag)
        {
            if (NeedsNewLine)
                NewLine();

            FontWeight fontWeight = (style.HasFlag(TextStyle.Bold) ? FontWeights.Bold : FontWeights.Normal);
            FontStyle fontStyle = (style.HasFlag(TextStyle.Italic) ? FontStyles.Italic : FontStyles.Normal);

            TextBlock textBlock;
            if (Measuring)
            {
                // Update the Height & Width of the current VM
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    // Determine the height and width of the text, caching common results.  We get good performance savings even
                    // if not caching comments and long string literals, and even if the cache isn't static (so only caches the
                    // current rendering).  However, memory usage for the string/double cache isn't too bad, and we get even
                    // better performance by just caching everything and having the cache itself be static (it gets cleared when
                    // the solution is unloaded, or if the code font is changed).
                    double width, height, baseline, underBaseline;
                    if (style.HasFlag(TextStyle.NoCache) || fontFamily != _codeFontFamily || fontSize != _codeFontSize || fontWeight != FontWeights.Normal || fontStyle != FontStyles.Normal)
                    {
                        // For comments or anything other than the code font, size, and normal style, calculate the height & width without caching
                        Typeface typeFace = (fontFamily == _proportionalFontFamily && fontWeight == FontWeights.Normal && fontStyle == FontStyles.Normal)
                            ? _proportionalFontTypeFace : new Typeface(fontFamily, fontStyle, fontWeight, FontStretches.Normal);
                        if (text.Length > 0 && text[0] == '\n')  // Workaround newline issue with FormattedText
                            text = "\n" + text;
                        FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, brush);
                        width = formattedText.WidthIncludingTrailingWhitespace;
                        height = formattedText.Height;
                        if (height == 0)
                        {
                            // For empty strings or control chars, use the height of a space of the same Typeface and size
                            formattedText = new FormattedText(" ", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, brush);
                            height = formattedText.Height;
                        }
                        // Adjust height if vertical position is negative
                        if (verticalOffset < 0)
                            height += verticalOffset;
                        // Very ugly way to check if only one line, since FormattedText doesn't tell you
                        if (height < formattedText.Baseline * 2)
                        {
                            baseline = formattedText.Baseline;
                            underBaseline = height - baseline;
                        }
                        else
                            baseline = underBaseline = 0;  // Ignore baseline calcs for multi line
                    }
                    else
                    {
                        // If it's the code font and size and normal style, look for a cached width for a big performance boost
                        if (string.IsNullOrEmpty(text))
                        {
                            width = 0;
                            height = _fontHeight;
                            baseline = _fontBaseline;
                            underBaseline = _fontHeight - _fontBaseline;
                        }
                        else if (_codeTextWidths.TryGetValue(text, out width))
                        {
                            height = _fontHeight;
                            baseline = _fontBaseline;
                            underBaseline = _fontHeight - _fontBaseline;
                        }
                        else
                        {
                            // Calculate the height & width
                            if (text.Length > 0 && text[0] == '\n')  // Workaround newline issue with FormattedText
                                text = "\n" + text;
                            FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, _codeFontTypeFace, _codeFontSize, brush);
                            width = formattedText.WidthIncludingTrailingWhitespace;
                            height = formattedText.Height;
                            // Correct issues with FormattedText - it gives an empty string zero height, and it doesn't add a line
                            // for a starting newline, only subsequent ones (TextBlock handles these correctly).
                            if (height == 0)
                                height = _fontHeight;
                            // Don't cache multi-line text so we don't have to cache the height (should only be comments, anyway)
                            if (height == _fontHeight)
                            {
                                _codeTextWidths.Add(text, width);
                                baseline = _fontBaseline;
                                underBaseline = _fontHeight - _fontBaseline;
                            }
                            else
                                baseline = underBaseline = 0;  // Ignore baseline calcs for multi line
                        }
                    }

                    // Handle baseline adjustments for different fonts on the same line
                    bool adjustHeight = false;
                    if (currentMeasureState.MaxBaseline < baseline)
                    {
                        currentMeasureState.MaxBaseline = baseline;
                        adjustHeight = true;
                    }
                    if (currentMeasureState.MaxUnderBaseline < underBaseline)
                    {
                        currentMeasureState.MaxUnderBaseline = underBaseline;
                        adjustHeight = true;
                    }
                    if (adjustHeight)
                    {
                        double lineHeight = currentMeasureState.MaxBaseline + currentMeasureState.MaxUnderBaseline;
                        if (height < lineHeight)
                            height = lineHeight;
                    }

                    if (currentMeasureState.LineHeight < height)
                        currentMeasureState.LineHeight = height;
                    currentMeasureState.LineWidth += width;
                }
                textBlock = null;
            }
            else
            {
                textBlock = new TextBlock
                                {
                                    Text = text,
                                    Foreground = brush,
                                    FontFamily = fontFamily,
                                    FontSize = fontSize,
                                    FontWeight = fontWeight,
                                    FontStyle = fontStyle,
                                    VerticalAlignment = VerticalAlignment.Center,
                                    SnapsToDevicePixels = true,
                                    Tag = tag
                                };
                if (style.HasFlag(TextStyle.Bold))
                    textBlock.FontWeight = FontWeights.Bold;
                if (style.HasFlag(TextStyle.Italic))
                    textBlock.FontStyle = FontStyles.Italic;
                if (tag != null)
                {
                    textBlock.MouseEnter += textBlock_MouseEnter;
                    textBlock.MouseLeave += textBlock_MouseLeave;
                    textBlock.MouseDown += textBlock_MouseDown;
                    if (tag.FrameworkElement == null)
                        tag.FrameworkElement = textBlock;
                }
                ++TextBlockCount;

                // Adjust vertical position if so requested (allows operators to be aligned properly)
                if (verticalOffset != 0)
                {
                    // The vertical offset is in pixels at the default 13 pixel size, so adjust it for other sizes,
                    // and then double it because we have vertical centering turned on.
                    double topMargin = (verticalOffset * (fontSize / 13));
                    textBlock.Margin = new Thickness(0, topMargin, 0, 0);
                    textBlock.Measure(new Size(double.MaxValue, double.MaxValue));
                    Size size = textBlock.DesiredSize;
                    // Set MaxHeight to clip it (doesn't include the Margin, but DesiredSize does, so subtract it twice)
                    textBlock.MaxHeight = size.Height - topMargin * 2;
                    size.Height -= topMargin;
                }

                if (_currentLine != null)
                    AddElementToCurrentLine(textBlock);
                else if (_currentBlock != null)
                    AddElementToCurrentBlock(textBlock);
                else if (_currentBorder != null)  // Ignore text put directly in a Canvas, such as spaces between statements on the same line
                    AddElementToCurrentBorder(textBlock);
                _isNewLine = false;
            }

            return textBlock;
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, TextStyle style, CodeObjectVM tag)
        {
            if (style.HasFlag(TextStyle.Proportional))
                return RenderText(text, brush, _proportionalFontFamily, _proportionalFontSize, 0, style, tag);
            return RenderText(text, brush, _codeFontFamily, _codeFontSize, 0, style, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, TextStyle style)
        {
            return RenderText(text, brush, style, null);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, int verticalOffset, CodeObjectVM tag)
        {
            return RenderText(text, brush, fontFamily, _codeFontSize, verticalOffset, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, FontFamily fontFamily, CodeObjectVM tag)
        {
            return RenderText(text, brush, fontFamily, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the specified text.
        /// </summary>
        public TextBlock RenderText(string text, Brush brush, CodeObjectVM tag)
        {
            return RenderText(text, brush, _codeFontFamily, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render a name, hiding any 'Attribute' suffix if it's an attribute name.
        /// </summary>
        public TextBlock RenderName(string name, Brush brush, CodeObjectVM tag, CodeObjectVM.RenderFlags flags)
        {
            // Hide any "Attribute" suffix for attribute constructor names
            if (flags.HasFlag(CodeObjectVM.RenderFlags.Attribute) && name.EndsWith(Attribute.NameSuffix))
                name = name.Substring(0, name.Length - Attribute.NameSuffix.Length);
            return RenderText(name, brush, tag);
        }

        /// <summary>
        /// Render the specified name and value.
        /// </summary>
        public void RenderNameValue(string name, string value)
        {
            NewLine();
            RenderText(name + ": ", CodeObjectVM.NORMAL_BRUSH, TextStyle.Proportional | TextStyle.Bold);
            RenderText(value, CodeObjectVM.NORMAL_BRUSH, TextStyle.Proportional);
        }

        /// <summary>
        /// The font used to render a right-arrow.
        /// </summary>
        public static FontFamily RightArrowFont = new FontFamily("Wingdings 3");

        /// <summary>
        /// Render a right-arrow.
        /// </summary>
        public void RenderRightArrow(Brush brush, CodeObjectVM tag)
        {
            RenderText("\x92 ", brush, RightArrowFont, _codeFontSize, 0, TextStyle.Normal, tag);
        }

        /// <summary>
        /// Render the image in the specified file name.
        /// </summary>
        public void RenderImage(string fileName, int height, int width, CodeObjectVM tag)
        {
            if (Measuring)
            {
                // Adjust size for zoom level (based on font size)
                double actualHeight = height * _zoomLevel;
                double actualWidth = width * _zoomLevel;

                // Update the Height & Width of the current VM
                MeasureState currentMeasureState = _measureStateStack.Peek();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != null)
                {
                    if (currentMeasureState.LineHeight < actualHeight)
                        currentMeasureState.LineHeight = actualHeight;
                    currentMeasureState.LineWidth += actualWidth;
                }
            }
            else
            {
                Image image = new Image
                    {
                        Source = new BitmapImage(new Uri(fileName, UriKind.Relative)),
                        Height = height,
                        Width = width,
                        Margin = new Thickness(2, 0, 4, 0),  // Hard-code left & right margins for now
                        Tag = tag
                    };
                image.MouseEnter += image_MouseEnter;
                image.MouseLeave += image_MouseLeave;

                if (_currentBlock == null)
                    CreateBlock(0);
                CreateLine(_currentBlock);
                AddElementToCurrentLine(image);
                _isNewLine = false;
            }
        }

        /// <summary>
        /// The number of <see cref="WrapPanel"/>s created.
        /// </summary>
        public int WrapPanelCount;

        /// <summary>
        /// Create a new 'line' of code.
        /// </summary>
        public WrapPanel CreateLine(FrameworkElement parent)
        {
            // Set the Tag of the WrapPanel to match its parent's Tag instead of the object that actually
            // creates it (this makes more sense considering the way that WrapPanels are used).
            WrapPanel wrapPanel = new WrapPanel { Tag = parent.Tag, VerticalAlignment = VerticalAlignment.Center };
            ++WrapPanelCount;

            _currentLine = wrapPanel;
            _currentLineWrapper = null;
            if (parent is Panel)  // Block
                ((Panel)parent).Children.Add(wrapPanel);
            else
                ((Border)parent).Child = wrapPanel;
            return wrapPanel;
        }

        /// <summary>
        /// The number of <see cref="StackPanel"/>s created.
        /// </summary>
        public int StackPanelCount;

        /// <summary>
        /// Create a new 'block' of code.
        /// </summary>
        public StackPanel CreateBlock(int indentSpaces)
        {
            if (Measuring) return null;

            StackPanel stackPanel = new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center };
            if (indentSpaces > 0)
                stackPanel.Margin = new Thickness(indentSpaces * SpaceWidth, 0, 0, 0);
            ++StackPanelCount;

            _isNewLine = true;  // Blocks are always on new lines
            if (_currentBlock != null)
            {
                _currentBlock.Children.Add(stackPanel);
                stackPanel.Tag = _currentBlock.Tag;
            }
            else //if (_currentBorder != null)
            {
                AddElementToCurrentBorder(stackPanel);
                stackPanel.Tag = _currentBorder.Tag;
            }
            _currentBlock = stackPanel;
            _currentLine = null;
            _currentLineWrapper = null;
            return stackPanel;
        }

        /// <summary>
        /// Create a <see cref="Canvas"/> for partial rendering of a <see cref="BlockVM"/>.
        /// </summary>
        public Canvas CreateCanvas(double height, double width)
        {
            // A Canvas should only be created as a new 'line' in an existing block, or part of an existing 'line' (after an indent)
            _currentCanvas = new Canvas { Height = height, Width = width };
            if (_currentLine != null)
                AddElementToCurrentLine(_currentCanvas);
            else if (_currentBlock != null)
                AddElementToCurrentBlock(_currentCanvas);
            else //if (_currentBorder != null)
                AddElementToCurrentBorder(_currentCanvas);
            _isNewLine = false;
            _currentBorder = null;
            _currentBlock = null;
            _currentLine = null;
            _currentLineWrapper = null;
            return _currentCanvas;
        }

        /// <summary>
        /// Set the current <see cref="Canvas"/> object (used for virtualized rendering).
        /// </summary>
        public void SetCurrentCanvas(Canvas canvas)
        {
            _currentCanvas = canvas;
        }

        /// <summary>
        /// The number of <see cref="Border"/>s created.
        /// </summary>
        public int BorderCount;

        /// <summary>
        /// Create a <see cref="Border"/>.
        /// </summary>
        public void CreateBorder(Brush borderBrush, Brush backgroundBrush, CodeObjectVM codeObjectVM, double leftIndent, int verticalPad, int horizontalPad)
        {
            // Determine border padding, accounting for zoom level (based on font size)
            bool usePadding = (ShowBackgroundColors || ShowBorders);
            double actualLeftPad, actualRightPad, actualVerticalPad;
            if (usePadding)
            {
                actualVerticalPad = verticalPad * _zoomLevel;
                actualLeftPad = (leftIndent > 0 ? (horizontalPad + leftIndent) : horizontalPad) * _zoomLevel;
                actualRightPad = horizontalPad * _zoomLevel;
            }
            else
            {
                actualVerticalPad = 0;
                actualLeftPad = (leftIndent > 0 ? (leftIndent * _zoomLevel) : 0);
                actualRightPad = 0;
            }

            if (Measuring)
            {
                // Clear the Height & Width, and push the new MeasureState on the stack
                codeObjectVM.Height = codeObjectVM.Width = 0;
                if (ShowBorders)
                {
                    actualLeftPad += _zoomLevel;
                    actualRightPad += _zoomLevel;
                    actualVerticalPad += _zoomLevel;
                }
                MeasureState measureState = new MeasureState(codeObjectVM, actualLeftPad, actualRightPad, actualVerticalPad);
                _measureStateStack.Push(measureState);
            }
            else
            {
                Border border = new Border
                    {
                        Tag = codeObjectVM,
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Center,
                        SnapsToDevicePixels = true, // Needed to prevent anti-aliasing
                        CornerRadius = new CornerRadius(_zoomLevel * 4)
                    };
                border.MouseEnter += border_MouseEnter;
                border.MouseLeave += border_MouseLeave;
                border.MouseDown += border_MouseDown;
                ++BorderCount;

                border.Padding = new Thickness(actualLeftPad, actualVerticalPad, actualRightPad, actualVerticalPad);

                if (ShowBorders)
                {
                    // Set border thickness, accounting for zoom level (based on font size)
                    border.BorderThickness = new Thickness(_zoomLevel);

                    if (UseShading && (borderBrush != Brushes.Transparent))
                    {
                        Color borderColor = ((SolidColorBrush)borderBrush).Color;
                        border.BorderBrush = new LinearGradientBrush(
                            new GradientStopCollection(3)
                            {
                                new GradientStop(Colors.LightGray, 0),
                                //new GradientStop(normal, 0.4),
                                new GradientStop(borderColor, 1)
                            },
                            new Point(0.5, 0), new Point(0.5, 1));
                    }
                    else
                        border.BorderBrush = borderBrush;
                }

                Color normalColor = (backgroundBrush is SolidColorBrush ? ((SolidColorBrush)backgroundBrush).Color : Colors.Black);
                Color lightColor = Colors.White;

                if (ShowBackgroundColors)
                {
                    if (UseShading && (backgroundBrush != Brushes.Transparent) && backgroundBrush is SolidColorBrush)
                    {
                        //Color.FromRgb(AddColor(dark.R, 24), AddColor(dark.G, 24), AddColor(dark.B, 24));
                        border.Background = new LinearGradientBrush(
                            new GradientStopCollection(3)
                            {
                                new GradientStop(normalColor, 0),
                                new GradientStop(lightColor, 0.4),
                                new GradientStop(normalColor, 1)
                            },
                            new Point(0.5, 0), new Point(0.5, 1));
                    }
                    else
                        border.Background = backgroundBrush;
                }
                else if (ShowBorders)
                    border.Background = Brushes.White;

                if (codeObjectVM != null && codeObjectVM.FrameworkElement == null)
                    codeObjectVM.FrameworkElement = border;

                if (_currentLine != null)
                    AddElementToCurrentLine(border);
                else if (_currentCanvas != null)
                    AddElementToCurrentCanvas(border, (codeObjectVM != null ? codeObjectVM.Y : 0), (codeObjectVM != null ? codeObjectVM.X : 0));
                else if (_currentBlock != null)
                    AddElementToCurrentBlock(border);
                else //if (_currentBorder != null)
                    AddElementToCurrentBorder(border);

                _isNewLine = false;
                _currentBorder = border;
                _currentBlock = null;
                _currentCanvas = null;
                _currentLine = null;
                _currentLineWrapper = null;
            }

            _borderIndentStateStack.Push(new BorderIndentState(codeObjectVM));
        }

        /// <summary>
        /// Create a <see cref="Border"/>.
        /// </summary>
        public void CreateBorder(Brush borderBrush, Brush backgroundBrush, CodeObjectVM codeObjectVM)
        {
            CreateBorder(borderBrush, backgroundBrush, codeObjectVM, 0, 1, 1);
        }

        protected void AddElementToLineWrapper(UIElement element)
        {
            InlineUIContainer container = new InlineUIContainer(element);
            if (element is TextBlock)
            {
                // If the text has a vertical adjustment, divide it by 2 because the baseline alignment overrides the
                // vertical centering that required that the offset be doubled.
                TextBlock textBlock = (TextBlock)element;
                if (textBlock.Margin.Top != 0)
                {
                    Thickness margin = textBlock.Margin;
                    textBlock.Margin = new Thickness(margin.Left, margin.Top / 2, margin.Right, margin.Bottom);
                }
            }
            else
            {
                // Nested non-TextBlocks (such as Border) don't get aligned based on nested TextBlock baselines inside them,
                // so tell WPF to center them within the parent instead.  This is off by a pixel for some reason (one pixel
                // above and 3 below in the parent) - one possible solution might be to make sure that the nested Border has
                // all different fonts of the parent represented within it, perhaps by adding hidden (NUL) characters.
                container.BaselineAlignment = BaselineAlignment.Center;
            }
            _currentLineWrapper.Inlines.Add(container);
        }

        protected void AddElementToCurrentLine(UIElement element)
        {
            // If we already have a wrapper, just add the element
            if (_currentLineWrapper != null)
                AddElementToLineWrapper(element);
            else
            {
                int lastIndex = _currentLine.Children.Count - 1;
                if (lastIndex >= 0)
                {
                    // If the font just changed, create a TextBlock wrapper to force alignment on the baselines of the children
                    UIElement lastTextBlock = _currentLine.Children[lastIndex];
                    while (!(lastTextBlock is TextBlock) && lastIndex > 0)
                        lastTextBlock = _currentLine.Children[--lastIndex];
                    if (lastTextBlock is TextBlock && element is TextBlock && ((TextBlock)lastTextBlock).FontFamily != ((TextBlock)element).FontFamily)
                    {
                        _currentLineWrapper = new TextBlock();
                        UIElementCollection children = _currentLine.Children;
                        while (children.Count > 0)
                        {
                            UIElement child = children[0];
                            children.RemoveAt(0);
                            if (child is Border)
                            {
                                // WPF seems to have some nasty issue that prevents Borders from being displayed once they are
                                // copied to the new wrapper (new Borders added later work OK).  Unable to figure out why... doesn't
                                // see to be a problem with the Parent or visual tree, and invalidating things doesn't work.
                                // Finally came up with a workaround of creating a new Border object, and that works.  WPF SUX!
                                Border oldBorder = (Border)child;
                                UIElement childElement = oldBorder.Child;
                                oldBorder.Child = null;
                                Border newBorder = new Border
                                    {
                                        Tag = oldBorder.Tag,
                                        HorizontalAlignment = oldBorder.HorizontalAlignment,
                                        VerticalAlignment = oldBorder.VerticalAlignment,
                                        SnapsToDevicePixels = oldBorder.SnapsToDevicePixels,
                                        CornerRadius = oldBorder.CornerRadius,
                                        Padding = oldBorder.Padding,
                                        BorderThickness = oldBorder.BorderThickness,
                                        BorderBrush = oldBorder.BorderBrush,
                                        Background = oldBorder.Background,
                                        Child = childElement
                                    };
                                if (newBorder.Tag is CodeObjectVM)
                                    ((CodeObjectVM)newBorder.Tag).FrameworkElement = newBorder;
                                newBorder.MouseEnter += border_MouseEnter;
                                newBorder.MouseLeave += border_MouseLeave;
                                newBorder.MouseDown += border_MouseDown;
                                AddElementToLineWrapper(newBorder);
                            }
                            else
                                AddElementToLineWrapper(child);
                        }
                        AddElementToLineWrapper(element);
                        element = _currentLineWrapper;
                    }
                }
                _currentLine.Children.Add(element);
            }
        }

        protected void AddElementToCurrentBlock(FrameworkElement element)
        {
            int lastIndex = _currentBlock.Children.Count - 1;
            if (_isNewLine || lastIndex < 0)
            {
                if (element != null)
                    _currentBlock.Children.Add(element);
                else
                    CreateLine(_currentBlock);
            }
            else
            {
                UIElement lastElement = _currentBlock.Children[lastIndex];  // Border or TextBlock
                _currentBlock.Children.RemoveAt(lastIndex);
                CreateLine(_currentBlock);
                AddElementToCurrentLine(lastElement);
                if (element != null)
                    AddElementToCurrentLine(element);
            }
        }

        protected void AddElementToCurrentCanvas(FrameworkElement element, double Y, double X)
        {
            if (element != null)
            {
                // Save the last Canvas index for use by virtualized rendering
                _currentCanvas.Children.Add(element);
                Canvas.SetTop(element, Y);
                if (X > 0)
                    Canvas.SetLeft(element, X);
            }
        }

        protected void AddElementToCurrentBorder(FrameworkElement element)
        {
            if (_currentBorder.Child == null)
            {
                if (element != null)
                    _currentBorder.Child = element;
                else
                    CreateLine(_currentBorder);
            }
            else
            {
                UIElement existingElement = _currentBorder.Child;
                _currentBorder.Child = null;
                if (_isNewLine)
                {
                    CreateBlock(0);
                    _currentBlock.Children.Add(existingElement);
                    if (element != null)
                        _currentBlock.Children.Add(element);
                }
                else
                {
                    if (existingElement is WrapPanel)  // Line
                    {
                        if (element != null)
                            ((Panel)existingElement).Children.Add(element);
                    }
                    else
                    {
                        CreateLine(_currentBorder);
                        AddElementToCurrentLine(existingElement);
                        if (element != null)
                            AddElementToCurrentLine(element);
                    }
                }
            }
        }

        /// <summary>
        /// Return to the parent of the current border.
        /// </summary>
        public void ReturnToBorderParent(CodeObjectVM codeObjectVM)
        {
            NeedsNewLine = false;
            if (Measuring)
            {
                // Pop the current VM, and update the Height & Width if necessary
                MeasureState currentMeasureState = _measureStateStack.Pop();
                CodeObjectVM currentVM = currentMeasureState.CodeObjectVM;
                if (currentVM != codeObjectVM)
                    throw new Exception("ERROR: PopMeasureState: VM doesn't match VM on stack!");
                currentVM.Height += currentMeasureState.LineHeight + (currentMeasureState.VerticalPad * 2);
                if (currentVM.Width < currentMeasureState.LineWidth)
                    currentVM.Width = currentMeasureState.LineWidth;
                currentVM.Width += currentMeasureState.LeftPad + currentMeasureState.RightPad;

                // Update the Height & Width of the parent VM (if any)
                MeasureState parentMeasureState = _measureStateStack.Peek();
                CodeObjectVM parentVM = parentMeasureState.CodeObjectVM;
                if (parentVM != null)
                {
                    if (parentMeasureState.LineHeight < currentVM.Height)
                        parentMeasureState.LineHeight = currentVM.Height;
                    parentMeasureState.LineWidth += currentVM.Width;
                }
            }
            else if (_currentBorder != null)
            {
                DependencyObject parent = _currentBorder.Parent;
                if (parent is InlineUIContainer)
                {
                    _currentLineWrapper = (TextBlock)((InlineUIContainer)parent).Parent;
                    _currentLine = (WrapPanel)_currentLineWrapper.Parent;
                    FindParentBlockOfLine();
                }
                else if (parent is WrapPanel)
                {
                    _currentLineWrapper = null;
                    _currentLine = (WrapPanel)parent;
                    FindParentBlockOfLine();
                }
                else if (parent is StackPanel)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentBlock = (StackPanel)parent;
                    FindParentBorderOfBlock();
                }
                else if (parent is Canvas)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentCanvas = (Canvas)parent;
                    _currentBlock = null;
                    _currentBorder = null;
                }
                else // if (parent is Border)
                {
                    _currentLineWrapper = null;
                    _currentLine = null;
                    _currentBlock = null;
                    _currentBorder = (Border)parent;
                }
            }

            _borderIndentStateStack.Pop();
        }

        /// <summary>
        /// Return to the parent of the current block.
        /// </summary>
        public void ReturnToBlockParent()
        {
            if (_currentBlock != null)
            {
                DependencyObject parent = _currentBlock.Parent;
                if (parent is StackPanel)
                {
                    _currentBlock = (StackPanel)parent;
                    UIElement lastElement = _currentBlock.Children[_currentBlock.Children.Count - 1];
                    _currentLine = (lastElement is WrapPanel ? (WrapPanel)lastElement : null);
                    if (_currentLine != null && _currentLine.Children.Count == 1
                        && _currentLine.Children[0] is TextBlock && ((TextBlock)_currentLine.Children[0]).Inlines.Count > 0)
                        _currentLineWrapper = (TextBlock)_currentLine.Children[0];
                    else
                        _currentLineWrapper = null;
                    FindParentBorderOfBlock();
                }
                // Do nothing if the parent is a Border
            }
        }

        /// <summary>
        /// Return to the parent of the current canvas.
        /// </summary>
        public void ReturnToCanvasParent()
        {
            if (_currentCanvas != null)
            {
                DependencyObject parent = _currentCanvas.Parent;
                if (parent is WrapPanel)
                {
                    _currentLine = (WrapPanel)parent;
                    parent = _currentLine.Parent;
                }
                if (parent is StackPanel)
                {
                    _currentBlock = (StackPanel)parent;
                    FindParentBorderOfBlock();
                }
                if (parent is Border)
                    _currentBorder = (Border)parent;
                _currentCanvas = null;
            }
        }

        protected void FindParentBlockOfLine()
        {
            if (_currentLine.Parent is StackPanel)
            {
                _currentBlock = (StackPanel)_currentLine.Parent;
                FindParentBorderOfBlock();
            }
            else // if (_currentLine.Parent is Border)
            {
                _currentBlock = null;
                _currentBorder = (Border)_currentLine.Parent;
            }
        }

        protected void FindParentBorderOfBlock()
        {
            for (DependencyObject parent = _currentBlock.Parent; ; )
            {
                if (parent is StackPanel)
                {
                    parent = ((StackPanel)parent).Parent;
                    continue;
                }
                if (parent is Border)
                    _currentBorder = (Border)parent;
                break;
            }
        }

        /// <summary>
        /// Get the current absolute Y position.
        /// </summary>
        public double GetCurrentAbsoluteY(CodeObjectVM codeObjectVM)
        {
            MeasureState measureState = _measureStateStack.Peek();
            if (codeObjectVM == null)
                codeObjectVM = measureState.CodeObjectVM;
            else
            {
                while (measureState.CodeObjectVM != codeObjectVM && !(codeObjectVM is CodeUnitVM))
                    codeObjectVM = codeObjectVM.ParentVM;
            }

            // Get the offset within the parent Canvas plus the top of the border and the current height
            double Y = 0;
            if (codeObjectVM != null)
            {
                Y = codeObjectVM.Y + measureState.VerticalPad + codeObjectVM.Height;

                // Add the absolute Y of the parent Canvas
                if (!(codeObjectVM is CodeUnitVM))
                {
                    CodeObjectVM parentBlockVM = codeObjectVM.ParentVM;
                    if (parentBlockVM != null)
                    {
                        BlockVM bodyVM = (parentBlockVM is IBlockVM ? ((IBlockVM)parentBlockVM).BodyVM : null);
                        if (bodyVM == null || !bodyVM.Contains(codeObjectVM) || bodyVM.Y == 0)
                        {
                            // If the parent wasn't an IBlockVM or it's not virtualized (Y is 0), we have to look recursively up
                            // the tree until we find one (this handles BlockVMs in expressions, such as for AnonymousMethodVM,
                            // or the case of a DocCodeVM with a single CodeObjectVM child instead of a BlockVM).   We also must do
                            // this if the CodeObjectVM doesn't exist in the BodyVM of the IBlockVM parent, which can occur if we
                            // came from a prefix, such as a block of code in a DocCodeVM in a DocCommentVM.
                            MeasureState topState = _measureStateStack.Pop();
                            Y += GetCurrentAbsoluteY(parentBlockVM);
                            _measureStateStack.Push(topState);
                        }
                        else
                            Y += bodyVM.Y;
                    }
                }
            }
            return Y;
        }

        /// <summary>
        /// Get the current absolute Y position.
        /// </summary>
        public double GetCurrentAbsoluteY()
        {
            return GetCurrentAbsoluteY(null);
        }

        /// <summary>
        /// Begin a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineBegin(CodeObjectVM codeObjectVM, int indentAmount)
        {
            if (indentAmount == 0)
                indentAmount = CodeObject.TabSize;
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            if (borderIndentState.IndentOnNewLine == 0)
                borderIndentState.PendingIndentState = new IndentState(codeObjectVM, indentAmount);
            ++borderIndentState.IndentOnNewLine;
        }

        /// <summary>
        /// Begin a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineBegin(CodeObjectVM codeObjectVM)
        {
            IndentOnNewLineBegin(codeObjectVM, 0);
        }

        /// <summary>
        /// End a section during which any newline should cause an indent.
        /// </summary>
        public void IndentOnNewLineEnd(CodeObjectVM codeObjectVM)
        {
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            if (borderIndentState.IndentOnNewLine > 0)
                --borderIndentState.IndentOnNewLine;
            else if (borderIndentState.IndentStateStack != null && borderIndentState.IndentStateStack.Count > 0)
            {
                if (borderIndentState.IndentStateStack.Peek().CodeObjectVM == codeObjectVM)
                    borderIndentState.IndentStateStack.Pop();
            }
        }

        /// <summary>
        /// Turn off indentation on new lines for the current level.
        /// </summary>
        public void IndentOnNewLineReset()
        {
            BorderIndentState borderIndentState = _borderIndentStateStack.Peek();
            borderIndentState.IndentOnNewLine = 0;
        }

        /// <summary>
        /// Render an EOL comment.
        /// </summary>
        /// <remarks>
        /// Unlike the CodeWriter for text output, we don't do deferred rendering of EOL comments because it gets too ugly with UI objects,
        /// and it's also not technically necessary (the format in this case is only cosmetic).  As with text, the comments will render with
        /// '//' by default, or retain '/* */' if parsed/marked as block comments.  Auto-cleanup might convert the latter to the former, and
        /// code editing will also convert between them as necessary.  Accidentally using '//' when a non-comment follows won't cause any
        /// problems in the GUI, unlike with text where it would be invalid.
        /// </remarks>
        public void RenderEOLComment(CommentVM commentVM)
        {
            // Preserve 'NeedsNewLine' state and clear it during this operation
            bool needsNewLine = NeedsNewLine;
            NeedsNewLine = false;
            commentVM.Render(this, CodeObjectVM.RenderFlags.None);
            NeedsNewLine = needsNewLine;
        }

        #endregion

        #region /* ACTIVATION (MOUSE-OVER) */

        protected const int InitialDelay = 400;
        protected const int BetweenDelay = 150;

        protected int _delay = InitialDelay;
        protected TextBlock _activeTextBlock;
        protected Border _activeBorder;
        protected readonly List<Border> _inBorders = new List<Border>();
        protected Brush _oldBorderBrush;
        protected Image _activeImage;
        protected CodeObjectVM _activeCodeObjectVM;
        protected DispatcherTimer _toolTipTimer;
        protected Popup _toolTipPopup;
        protected CodeRenderer _toolTipRenderer;

        protected static DropShadowEffect DropShadowEffect = new DropShadowEffect { BlurRadius = 2, Color = Colors.DarkGray, Direction = 0, ShadowDepth = 0 };

        protected void InitializeToolTipTimer()
        {
            _delay = InitialDelay;
            _toolTipTimer = new DispatcherTimer();
            _toolTipTimer.Tick += toolTipTimerElapsed;
        }

        public void ResetToolTipTimer()
        {
            _toolTipTimer.Stop();
            _toolTipTimer.Interval = new TimeSpan(0, 0, 0, 0, _delay);
            _toolTipTimer.Start();
        }

        public void StopToolTipTimer()
        {
            _toolTipTimer.Stop();
        }

        protected void ActivateText(TextBlock textBlock, bool isMouseDirectlyOver)
        {
            DeactivateText();
            _activeTextBlock = textBlock;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = textBlock.Tag as CodeObjectVM;

            // Apply a shadow effect to show activation
            textBlock.Effect = DropShadowEffect;
        }

        protected void DeactivateText()
        {
            if (_activeTextBlock != null)
            {
                _activeTextBlock.Effect = null;
                _activeTextBlock = null;
            }
        }

        protected void ActivateBorder(Border border, bool isMouseDirectlyOver)
        {
            DeactivateBorder();
            _activeBorder = border;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = (CodeObjectVM)border.Tag;

            // Change the border line to show activation (bitmap effects are too slow for this)
            _oldBorderBrush = border.BorderBrush;
            border.BorderBrush = Brushes.Blue;
        }

        protected void DeactivateBorder()
        {
            if (_activeBorder != null)
            {
                _activeBorder.BorderBrush = _oldBorderBrush;
                _activeBorder = null;
            }
        }

        protected void ActivateImage(Image image, bool isMouseDirectlyOver)
        {
            DeactivateImage();
            _activeImage = image;
            if (isMouseDirectlyOver)
                _activeCodeObjectVM = (CodeObjectVM)image.Tag;

            // Add a dropshadow effect
            image.Effect = new DropShadowEffect();
        }

        protected void DeactivateImage()
        {
            if (_activeImage != null)
            {
                _activeImage.Effect = null;
                _activeImage = null;
            }
        }

        public void DisplayToolTip()
        {
            UIElement element = (_activeImage ?? (UIElement)_activeTextBlock ?? _activeBorder);
            if (element != null)
            {
                StackPanel panel = new StackPanel { Tag = _activeCodeObjectVM };
                Border border = new Border
                    {
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Center,
                        SnapsToDevicePixels = true, // Needed to prevent anti-aliasing
                        CornerRadius = new CornerRadius(_zoomLevel * 4),
                        BorderThickness = new Thickness(_zoomLevel),
                        Padding = new Thickness(_zoomLevel * 3),
                        Margin = new Thickness(0, 0, 4, 4), // Leave margin for drop shadow
                        Effect = new DropShadowEffect { Color = Colors.Gray, Opacity = 0.4, ShadowDepth = 4 },
                        BorderBrush = new SolidColorBrush(Colors.Gray),
                        Background = new LinearGradientBrush(
                            new GradientStopCollection(3)
                                {
                                    new GradientStop(Colors.WhiteSmoke, 0),
                                    new GradientStop(Colors.White, 0.4),
                                    new GradientStop(Colors.WhiteSmoke, 1)
                                },
                            new Point(0.5, 0), new Point(0.5, 1)),
                        Child = panel,
                        Tag = _activeCodeObjectVM
                    };

                Point offset = Mouse.GetPosition(element);
                _toolTipPopup = new Popup
                    {
                        AllowsTransparency = true,
                        IsOpen = true,
                        PlacementTarget = element,
                        Placement = PlacementMode.Relative,
                        HorizontalOffset = offset.X + 8,
                        VerticalOffset = offset.Y + 10,
                        Child = border,
                        Tag = _activeCodeObjectVM
                    };
                foreach (object commandBinding in CommandBindings)
                    _toolTipPopup.CommandBindings.Add((CommandBinding)commandBinding);
                _toolTipPopup.ContextMenuOpening += ToolTipContextMenuOpening;

                _toolTipRenderer = new CodeRenderer(panel) { ToolTipContextMenuOpening = ToolTipContextMenuOpening, CommandBindings = CommandBindings};

#if SHOW_MEASUREMENTS
                if (element is Border)
                    renderer.RenderNameValue("Border Height-Width", string.Format("{0:F2} - {1:F2}", element.ActualHeight, element.ActualWidth));
                renderer.RenderNameValue("Height-Width", string.Format("{0:F2} - {1:F2}, Y={2:F2}", codeObjectVM.Height, codeObjectVM.Width, codeObjectVM.Y));
                if (codeObjectVM is IBlockVM)
                {
                    BlockVM blockVM = ((IBlockVM)codeObjectVM).BodyVM;
                    if (blockVM != null)
                        renderer.RenderNameValue("Block Height-Width", string.Format("{0:F2} - {1:F2}, Y={2:F2}", blockVM.Height, blockVM.Width, blockVM.Y));
                }
                renderer.NewLine();
#endif
                // Create a new VM to render the tooltip, because the height/width will differ for a description
                CodeObjectVM.CreateVM(_activeCodeObjectVM.CodeObject, _activeCodeObjectVM.ParentVM, true).RenderToolTip(_toolTipRenderer);

                _delay = BetweenDelay;
            }
        }

        protected void ClearActives()
        {
            _activeTextBlock = null;
            _activeBorder = null;
            _activeCodeObjectVM = null;
        }

        public void CloseToolTip(bool resetTimer, bool clearActives)
        {
            if (_toolTipPopup != null)
            {
                _toolTipRenderer.CloseToolTip(false, true);
                _toolTipPopup.IsOpen = false;
                _toolTipPopup = null;
                _delay = InitialDelay;
            }
            if (resetTimer)
                ResetToolTipTimer();
            if (clearActives)
                ClearActives();
        }

        public void CloseToolTip()
        {
            CloseToolTip(false, false);
        }

        #endregion

        #region /* EVENTS */

        protected void textBlock_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            TextBlock textBlock = (TextBlock)sender;

            // Upon entering a text block, clear any other active objects and make
            // the text block active.  We should never have nested text blocks.
            ActivateText(textBlock, textBlock.IsMouseDirectlyOver);
        }

        protected void textBlock_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            TextBlock textBlock = (TextBlock)sender;

            // Upon exiting a text block, deactivate any currently active text block IF it's
            // the same one (should always be, but check to be sure).
            if (textBlock == _activeTextBlock)
            {
                DeactivateText();

                // Set the active object to that of any parent border
                if (_activeBorder != null)
                    _activeCodeObjectVM = _activeBorder.Tag as CodeObjectVM;
            }
        }

        protected void textBlock_MouseDown(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            CloseToolTip();
        }

        protected void border_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Border border = (Border)sender;
            _inBorders.Add(border);  // Maintain list of borders we are inside
            if (_activeBorder == null || border.ActualWidth < _activeBorder.ActualWidth)
                ActivateBorder(border, border.IsMouseDirectlyOver);
        }

        protected void border_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Border border = (Border)sender;
            _inBorders.Remove(border);  // update list of borders we are inside
            if (border == _activeBorder)
            {
                DeactivateBorder();

                // Find and activate the next outer border, or close the tooltip if the mouse isn't over it
                // (meaning we've gone outside of the code window area).
                Border top = null;
                foreach (Border b in _inBorders)
                {
                    if (top == null || b.ActualWidth < top.ActualWidth)
                        top = b;
                }
                if (top != null)
                    ActivateBorder(top, top.IsMouseDirectlyOver);
            }
        }

        protected void border_MouseDown(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            CloseToolTip();
        }

        protected void image_MouseEnter(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Image image = (Image)sender;

            // Upon entering an image, clear any other active objects and make
            // the image active.
            ActivateImage(image, image.IsMouseDirectlyOver);
        }

        protected void image_MouseLeave(object sender, MouseEventArgs e)
        {
            e.Handled = true;
            Image image = (Image)sender;

            // Upon exiting an image, deactivate any currently active image IF it's
            // the same one (should always be, but check to be sure).
            if (image == _activeImage)
            {
                DeactivateImage();

                // Set the active object to that of any parent border
                if (_activeBorder != null)
                    _activeCodeObjectVM = _activeBorder.Tag as CodeObjectVM;
            }
        }

        protected void stackPanel_MouseMove(object sender, MouseEventArgs e)
        {
            ResetToolTipTimer();
        }

        protected void toolTipTimerElapsed(object sender, EventArgs e)
        {
            _toolTipTimer.Stop();

            // Ignore if the mouse is over the tooltip
            if (_toolTipPopup == null || !_toolTipPopup.IsMouseOver)
            {
                // Close any existing tooltip
                if (_toolTipPopup != null)
                    CloseToolTip();
                if (_toolTipPopup == null && _activeCodeObjectVM != null)
                {
                    // Open a new tooltip if appropriate
                    CodeObjectVM topMostVM = _topMost.Tag as CodeObjectVM;
                    if (topMostVM == null || topMostVM.CodeObject != _activeCodeObjectVM.CodeObject)
                        DisplayToolTip();
                }
            }
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand ShowBordersCommand = new RoutedCommand("Show Borders", typeof(CodeObjectVM));
        public static readonly RoutedCommand ShowBackgroundColorsCommand = new RoutedCommand("Show Background Colors", typeof(CodeObjectVM));
        public static readonly RoutedCommand UseShadingCommand = new RoutedCommand("Use Shading", typeof(CodeObjectVM));
        public static readonly RoutedCommand HalfHeightBlankLinesCommand = new RoutedCommand("Half-height Blank Lines", typeof(CodeObjectVM));
        public static readonly RoutedCommand MaximizeBordersCommand = new RoutedCommand("Maximize Use Of Borders", typeof(CodeObjectVM));

        private static void InitializeCommands()
        {
            //ShowBackgroundColorsCommand.InputGestures.Add(new KeyGesture(Key.O, ModifierKeys.Alt));
        }

        public static void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, ShowBordersCommand, showBorders_Executed);
            WPFUtil.AddCommandBinding(window, ShowBackgroundColorsCommand, showBackgroundColors_Executed);
            WPFUtil.AddCommandBinding(window, UseShadingCommand, useShading_Executed);
            WPFUtil.AddCommandBinding(window, HalfHeightBlankLinesCommand, halfHeightBlankLines_Executed);
            WPFUtil.AddCommandBinding(window, MaximizeBordersCommand, maximizeBorders_Executed);
        }

        private static void showBorders_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ShowBorders = !ShowBorders;
            ReRenderAll();
        }

        private static void showBackgroundColors_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ShowBackgroundColors = !ShowBackgroundColors;
            ReRenderAll();
        }

        private static void useShading_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            UseShading = !UseShading;
            ReRenderAll();
        }

        private static void halfHeightBlankLines_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            HalfHeightBlankLines = !HalfHeightBlankLines;
            ReRenderAll();
        }

        private static void maximizeBorders_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MaximizeBorders = !MaximizeBorders;
            ReRenderAll();
        }

        #endregion

        #region /* INDENT STATE */

        /// <summary>
        /// State information for indentation within the current border.
        /// </summary>
        protected class BorderIndentState
        {
            public int IndentOnNewLine;
            public CodeObjectVM BaseCodeObjectVM;
            public IndentState PendingIndentState;
            public Stack<IndentState> IndentStateStack = new Stack<IndentState>();

            public BorderIndentState(CodeObjectVM baseCodeObjectVM)
            {
                BaseCodeObjectVM = baseCodeObjectVM;
            }
        }

        /// <summary>
        /// State information for indentation within the current border.
        /// </summary>
        protected class IndentState
        {
            public CodeObjectVM CodeObjectVM;
            public int IndentPosition;

            public IndentState(CodeObjectVM codeObjectVM, int indentPosition)
            {
                CodeObjectVM = codeObjectVM;
                IndentPosition = indentPosition;
            }
        }

        #endregion
        
        #region /* MEASURE STATE */

        /// <summary>
        /// Measurement information for each nested <see cref="CodeObjectVM"/> being rendered.
        /// </summary>
        protected class MeasureState
        {
            public CodeObjectVM CodeObjectVM;
            public double LeftPad;
            public double RightPad;
            public double VerticalPad;
            public double LineHeight;
            public double LineWidth;
            public double MaxBaseline;
            public double MaxUnderBaseline;

            public MeasureState(CodeObjectVM codeObjectVM, double leftPad, double rightPad, double verticalPad)
            {
                CodeObjectVM = codeObjectVM;
                LeftPad = leftPad;
                RightPad = rightPad;
                VerticalPad = verticalPad;
            }
        }

        #endregion
    }
}
